/**   
 * @Title: SaveImageImpl.java 
 * @Package net.gdface.service.client 
 * @Description: TODO 
 * @author guyadong   
 * @date 2014-11-19 上午9:59:43 
 * @version V1.0   
 */
package net.gdface.service.client;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.facelib.u4fdb.localdb.TaskBean;
import net.facelib.u4fdb.localdb.UrlBean;
import net.gdface.facedb.db.ImageBean;
import net.gdface.facedb.DuplicateRecordException;
import net.gdface.facedb.FaceDb;
import net.gdface.httpclient.HostManager;
import net.gdface.image.ImageErrorException;
import net.gdface.image.LazyImage;
import net.gdface.image.NotImageException;
import net.gdface.image.UnsupportedFormatException;
import net.gdface.sdk.CodeInfo;
import net.gdface.sdk.NotFaceDetectedException;
import net.gdface.service.search.ImgInfo;
import net.gdface.service.search.RemoteImage;
import net.gdface.utils.Assert;
import net.gdface.utils.SimpleTypes;
import net.gdface.utils.Judge;
import net.gdface.utils.PathUtility;
import net.gdface.worker.WorkData;

import static net.gdface.db.DaoManagement.DAO;

/**
 * @author guyadong
 * 
 */
public class SaveImageImpl extends FetchAbstract   {
	static final Logger logger = LoggerFactory.getLogger(SaveImageImpl.class);
	private static final int MAX_URL_LENGTH= 256;
	private static final int MAX_RETRY=2;
	private final FaceDb facedb;
	private final int checkFlag;
	private final File root;
	private static int imgMinWidth = 128;
	private static int imgMinHeight = 128;
	private static int imgMaxWidth = 2048;
	private static int imgMaxHeight = 2048;
	private static int addImageFaceNum = 0;
	/**
	 * 全局结束标志
	 */
	private static AtomicBoolean isFinished = null;
	public static void setImageSizeLimit( int minWidth,int minHeight,int maxWidth,int maxHeight){
		imgMinWidth=minWidth;
		imgMinHeight=minHeight;
		imgMaxWidth=maxWidth;
		imgMaxHeight=maxHeight;
		RemoteImage.setImageSizeLimit(minWidth, minHeight, maxWidth, maxHeight);
	}
	public static void setAddImageParameters( int faceNum,boolean overwrite){
		addImageFaceNum=faceNum;
	}
	public static void setGlobalFinishedSignalObject(AtomicBoolean finished) {
		Assert.notNull(finished, "finished");
		if(null==isFinished)
			isFinished=finished;
	}

	public SaveImageImpl(File root, int checkFlag, FaceDb faceClient) {
		Assert.notNull(root, "root");
		Assert.notNull(faceClient, "faceClient");
		this.root = root;
		this.checkFlag = checkFlag;
		this.facedb=faceClient;
	}


	private boolean isDupImgInFacedb(String imgMd5) {
		Assert.notEmpty(imgMd5, "imgMd5");
		return facedb.getImage(imgMd5) != null;
	}

	private boolean needCheck(int flag) {
		return (this.checkFlag & flag) == flag;
	}

	/**
	 * 根据{@code md5}在数据库TASK表中查找指定的文件<br>
	 * 如果文件存在且长度不为0，则返回该文件，否则返回{@code null}
	 * @param woc 工作数据对象
	 * @param md5 MD5校验码
	 * @return 
	 */
	private File getFileByMD5(WorkData woc,String md5){
		assert null!=woc;
		File localFile=null;
		TaskBean taskBean;
		if (!Judge.isEmpty(md5)) {
			if(null!=(taskBean = this.loadTaskBeanVariables(woc, md5))){
				File f=new File(PathUtility.getAbsolutePath(this.root, taskBean.getFile()));
				if(f.exists()&&f.isFile()&&0<f.length()){
					localFile=f;
				}
			}
		}
		return localFile;
	}
	
	/**
	 * 根据{@code md5}校验码在数据库的GF_TASK表中搜索对应的记录{@link TaskBean}<br>
	 * 如果{@link WorkData}中已经定义该对象，则直接返回，否则从数据库中查找;
	 * 
	 * @param woc
	 * @param md5
	 * @return {@link TaskBean}对象，如果没有找到则返回null
	 */
	private final TaskBean loadTaskBeanVariables(WorkData woc,String md5){
		assert null!=woc;
		TaskBean taskBean=woc.getVariables(WorkDataJSON.VAR_TASKBEAN);
		if(null==taskBean){
			taskBean = DAO.daoGetTaskByIndexMd5(md5);
			woc.setVariables(WorkDataJSON.VAR_TASKBEAN,taskBean);
		}
		return taskBean;
	}

	@Override
	public boolean needSave(WorkData woc) {

		Status status = Status.NULL;
		RemoteImage rmi = woc.getVariables(WorkDataJSON.VAR_REMOTEIMAGE);
		assert null != rmi;
		UrlBean urlBean = woc.getVariables(WorkDataJSON.VAR_URLBEAN);
		TaskBean taskBean;
		LazyImage image=rmi.getImage();
		if (!rmi.isValidImage()) {
			//assert (rmi.getStatus() != Status.NULL);
			if(rmi.getStatus() == Status.NULL)
				logger.error("null");
			status = rmi.getStatus();
		} else if (Status.SAVEDB_NORMAL_STATUS.contains(Status.valueOf(urlBean.getStatus()))) {
			return false;
		} else if (needCheck(SaveImage.CHECK_DUP_LOCAL)
				&& null != (taskBean = loadTaskBeanVariables(woc, image.getMd5()))) {
			Status ts = Status.valueOf(taskBean.getStatus());
			if (Status.SAVEDB_NORMAL_STATUS.contains(ts))// 正常保存到数据库
				status = ts;
			//else 没有正常保存到数据库的，则需要保存
		} else if (needCheck(SaveImage.CHECK_DUP_IN_FACEDB) && this.isDupImgInFacedb(image.getMd5())) {
			status = Status.DUPINDB;
		}
		// 这里不能用else if
		Boolean checkFlag=woc.getVariables(WorkDataJSON.VAR_CHECKFACE);		
		if (Status.NULL == status) {
			//为提高效率减少重复的数据传输，只有不向数据库添加记录时(SaveImage.CHECK_DUP_IN_FACEDB标志位为0)
			//或WorkDataJSON.VAR_CHECKFACE标志为true 才预检测人脸，否则图像数据要上传到服务器两次
			if(Boolean.TRUE == checkFlag||!needCheck(SaveImage.CHECK_DUP_IN_FACEDB)){				
				try {
					boolean hasFace = faceDetected(woc, rmi.getImage());
					status=hasFace?Status.FACE_IMG:Status.NOT_FACE;
				} catch (ImageErrorException e) {
					status=Status.ERRIMAGE;
				}
			}
		}
		/*if (status != Status.NULL) {
			logger.debug(String.format("[%s]%s %s %s %s", rmi.isRetryed() ? "R" : "N", status.getMsg(),
					rmi.getException(), rmi.getUri(), rmi.getReferer()));
		}*/

		if (status != Status.NULL)
			urlBean.setStatus(status.name());
		if (rmi.getException() != null)
			urlBean.setExp(rmi.getException());
		if (image!= null)
			urlBean.setMd5(image.getMd5());

		return status == Status.NULL||Status.FACE_IMG==status;
	}

	private boolean faceDetected(WorkData woc, LazyImage img) throws ImageErrorException {
		net.gdface.sdk.CodeInfo[] codes=woc.getVariables(WorkDataJSON.VAR_CODES);
		if (null == codes) {
			byte[] imgBytes = img.getImgBytes();
			assert null != imgBytes && 0 < imgBytes.length;
			codes = this.facedb.detectAndGetCodeInfo(imgBytes);
			assert (codes != null);
			woc.setVariables(WorkDataJSON.VAR_CODES, codes);
			
		}
		return codes.length > 0;
	}
	
	@Override
	public void save(WorkData woc) {
		if(needCheck(SaveImage.CHECK_DUP_IN_FACEDB))
			save2Db(woc);

		if (needCheck(SaveImage.CHECK_DUP_LOCAL))
			saveLocal(woc);

	}	
	private final UrlBean initRetry(UrlBean bean){
		if(null==bean.getValue()){
			bean.setValue(1);
		}
		return bean;
	}
	
	/**根据状态(STATUS)和异常(EXP)字段判断是否需要重新尝试下载
	 * @param bean
	 * @return
	 */
	private boolean needRetry(UrlBean bean) {
		Status status = Status.valueOf(bean.getStatus());
		switch (status) {
		case TIMEOUT:
			return initRetry(bean).getValue() < MAX_RETRY;
		case FAIL_DOWNLOAD:
			return false;
		case TOO_LARGE://如果修改了图片过滤尺寸，就要重新对大尺寸照片进行处理
		case NEW:
		case FAIL_SAVEDB:	
		case UNEXP:
		case NULL:
		case FAIL_SAVELOCAL:
			return true;
		default:
			return false;
		}
	}
	
	

	@Override
	public boolean needDownload(WorkData woc) {
		ImgInfo imginfo = (ImgInfo) woc.getVariables(WorkDataJSON.VAR_IMGINFO);
		URI uri = imginfo.getLocaction();
		int width = imginfo.getWidth();
		int height = imginfo.getHeight();

		boolean need = false;
		UrlBean urlBean = null;
		Status status = Status.NULL;
		if (HostManager.getInstance().isALiveHost(uri)) {
			// 超长的url视为无效url
			if (uri.toString().length() > MAX_URL_LENGTH) {
				status = Status.ERRORURL;
			} else {
				if (null != (urlBean = DAO.daoGetUrl(uri.toString()))	) {
					//数据库中能找到记录
					status = Status.valueOf(urlBean.getStatus());
					if ((need = needRetry(urlBean))) {
						// 在此区分重试任务中哪些不需要下载
						String md5 = urlBean.getMd5();
						File locaFile = getFileByMD5(woc, md5);
						// 对无需下载的任务，生成RemoteImage，以执行保存操作
						if (!(need = (null == locaFile))) {
							try {
								RemoteImage rmi = new RemoteImage(imginfo.getLocaction(), imginfo.getReferer(),
										locaFile, md5);
								woc.setVariables(WorkDataJSON.VAR_REMOTEIMAGE, rmi);
							} catch (FileNotFoundException e) {
								throw new RuntimeException(e);
							} catch (UnsupportedFormatException e) {
								throw new RuntimeException(e);
							} catch (NotImageException e) {
								throw new RuntimeException(e);
							}
						}
					}
				} else {
					//数据库中没有记录
					// 超大/超小的图片不下载
					need = (Status.NULL == (status = chekSize(width, height)));
					/*if(!need){
						logger.debug(String.format("SKIP:%s [%dx%d] %s", status.getMsg(), width, height, uri));
					}*/
				}
			}
		} else
			status = Status.DEADHOST;
		if(null==urlBean){
			urlBean=UrlBean.builder().url(uri.toString()).status(status.name()).build();
		}
/*		if (!need) {
			logger.debug(String.format("SKIP:%s [%dx%d] %s", status.getMsg(), width, height, uri));
		}*/
		woc.setVariables(WorkDataJSON.VAR_URLBEAN, urlBean);
		return need;
	}

	@Override
	public void recordStatus(WorkData woc) {
		try {
			UrlBean urlBean = woc.getVariables(WorkDataJSON.VAR_URLBEAN);
			if (null != urlBean) {
				ImgInfo imginfo = woc.getVariables(WorkDataJSON.VAR_IMGINFO);
				assert null!=imginfo;
				Status status = Status.valueOf(urlBean.getStatus());
				if(urlBean.beModified()&&status!=Status.SAVED_INDB){
					logger.info("[{}]{} {} {} {}", imginfo.isRetryed() ? "R" : "N", urlBean.getStatus(), urlBean.getExp(),
							imginfo.getLocaction(), imginfo.getReferer());
				}
				// DEADHOST和ERRORURL两种状态的bean不保存
				if (Status.DEADHOST != status && Status.ERRORURL != status) {
					if (null != urlBean.getValue() && urlBean.beModified() && !Status.SAVEDB_NORMAL_STATUS.contains(status)){
						urlBean.setValue(urlBean.getValue() + 1);
					}
					//如果urlBean在数据库已经存在，则判断status是不是更大,如果是则更新
					//此处需要依赖Status中各种状态的定义顺序
					//程序别的地方存在bug,造成有可能内存表中数据不同步，此处修改是权宜之计，
					UrlBean oldBean = DAO.daoAddUrlIfAbsent(urlBean);
					if(oldBean != urlBean){						
						logger.warn("程序中存在数据不同步");
						if(status.compareTo(Status.valueOf(oldBean.getStatus()))>0){
							urlBean.setNew(false);
							DAO.daoSaveUrl(urlBean);
						}
					}
				} else {
					DAO.daoDeleteUrl(urlBean);
				}
			}
			TaskBean taskBean = woc.getVariables(WorkDataJSON.VAR_TASKBEAN);
			if (null != taskBean)
				DAO.daoSaveTask(taskBean);

		} finally {
		}
	}

	private Status chekSize(int width, int height) {
		Status status = Status.NULL;
		if (width > 0 && height > 0) {
			if (width < imgMinWidth || height < imgMinHeight)
				status = Status.TOO_SMALL;
			if (width * height > imgMaxWidth * imgMaxHeight)
				status = Status.TOO_LARGE;
		}
		return status;
	}

	@Override
	public void onRuntimeException(RuntimeException e, WorkData woc) throws RuntimeException {
		Throwable fault = SimpleTypes.stripThrowableShell(e, RuntimeException.class);
		//对于特定异常更新utlBean和taskBean状态
		TaskBean taskBean=woc.getVariables(WorkDataJSON.VAR_TASKBEAN);
		UrlBean urlBean=woc.getVariables(WorkDataJSON.VAR_URLBEAN);
		RemoteImage rm=woc.getVariables(WorkDataJSON.VAR_REMOTEIMAGE);
		if(null!=urlBean){
			if (Status.NULL == Status.valueOf(urlBean.getStatus())) {
				urlBean.setStatus(Status.UNEXP.name());
				urlBean.setExp(fault.getClass().getCanonicalName());
			}
		}
		if(null!=taskBean){
			taskBean.setExp(fault.getClass().getCanonicalName());
		}
		if (null != rm)
			logger.error("url=[{}] referer=[{}]",rm.getUri(), rm.getReferer());
		LocalApp.filterWebserviceFault(e,logger);
	}

	/**
	 * 保存图像到FaceDataBase
	 * @param woc
	 */
	private void save2Db(WorkData woc) {
		RemoteImage rmi=woc.getVariables(WorkDataJSON.VAR_REMOTEIMAGE);
		UrlBean urlBean=woc.getVariables(WorkDataJSON.VAR_URLBEAN);
		Assert.notNull (urlBean,"urlBean");
		if (!Status.SAVEDB_NORMAL_STATUS.contains(Status.valueOf(urlBean.getStatus()))) {
			TaskBean taskBean=this.loadTaskBeanVariables(woc, rmi.getImage().getMd5());
			assert (urlBean.getUrl().equals(rmi.getUri().toString()));

			if(null != taskBean){
				
				//如果抛出RuntimeException就会在onRuntimeException方法中置成对应的异常类名
				Status taskStatus = Status.valueOf(taskBean.getStatus());
				//判断该图片是不是曾经正常保存到数据库
				if (Status.SAVEDB_NORMAL_STATUS.contains(taskStatus)){
					//如果曾经已经保存到数据库，则更新状态后返回
					urlBean.setStatus(taskStatus.name());
					return;
				}
				//将exp先置为null,如果正常结束，就为null
				taskBean.setExp(null);
			}
	
			CodeInfo[] codes = woc.getVariables(WorkDataJSON.VAR_CODES);
			if (!Judge.isEmpty(codes)){
				urlBean.setValue(codes.length);
			}
			urlBean.setStatus(Status.FAIL_SAVEDB.name());
			byte[] imgBytes = rmi.getImage().getImgBytes();
			try {
				ImageBean res;
				if (Judge.isEmpty(codes)) {
					res = facedb.detectAndAddFeatures(imgBytes, addImageFaceNum);
				}else{
					res = facedb.addImage(imgBytes, Arrays.asList(codes));
				}
				assert null != res;
				urlBean.setStatus(Status.SAVED_INDB.name());
				logger.info("[{}] [{}]faces {} ,url[{}]", rmi.isRetryed() ? "R" : "N", urlBean.getStatus(),
						res.getFaceNum(), rmi.getUri(), urlBean.getUrl());
				urlBean.setValue(res.getFaceNum().intValue());
			} catch (DuplicateRecordException e) {
				urlBean.setStatus(Status.DUPINDB.name());
				urlBean.setExp(null);
				logger.info("{} {} ", urlBean.getStatus(), rmi.getUri());
			} catch (ImageErrorException e) {
				urlBean.setStatus(Status.ERRIMAGE.name());
				logger.info("{} exception:{},{} ", urlBean.getStatus(), e.getMessage(), rmi.getUri());
			} catch (NotFaceDetectedException e) {
				urlBean.setStatus(Status.NOT_FACE.name());
				logger.info("{} {} ", urlBean.getStatus(), rmi.getUri());
			} finally {
				//更新urlBean状态				
				//urlBean.setStatus(taskBean.getStatus());
			}
		}
	}
	/**
	 * 保存图像到本地文件
	 * @param woc
	 */
	private void saveLocal(WorkData woc) {
		UrlBean urlBean=woc.getVariables(WorkDataJSON.VAR_URLBEAN);
		try {
			ImgInfo imginfo=woc.getVariables(WorkDataJSON.VAR_IMGINFO);
			RemoteImage img=woc.getVariables(WorkDataJSON.VAR_REMOTEIMAGE);
			String md5Ref=imginfo.getMd5Ref();
			Status status=Status.valueOf(urlBean.getStatus());
			//无效图像
			if(!img.isValidImage())
				return;
			// 如果file不为null，说明已经本地文件已经成功保存
			String md5=img.getImage().getMd5();
			if(null  != getFileByMD5(woc,md5))
				return;
			//判断是否是有人脸的照片
			if (!(Status.SAVED_INDB == status || Status.DUPINDB == status || Status.FACE_IMG == status)) {
				// 这种情况下，必须检测过人脸，否则前面的逻辑肯定有错误
				//要么在saveDb时通过服务器提取过特征码。要么在needSave用本地SDK提取过特征码
				return ;
			}

			File workFolder = new File(this.root, imginfo.getKeyword());
			File folder = new File(workFolder, Judge.isEmpty(md5Ref) ? md5 : md5Ref);
			assert urlBean.getUrl().equals(img.getUri().toString());
	
			File saved = img.getImage().save(folder);
			urlBean.setExp(null);// 成功状态下，exp置为null;
			TaskBean taskBean = TaskBean.builder()
					.md5(md5)
					.value(urlBean.getValue())
					.status(urlBean.getStatus())
					.exp(null)/** 成功状态下，exp置为null */
					.file(PathUtility.getRelativeDbPath(this.root, saved.getAbsoluteFile())).build();
			// 只有存储成功，taskBean才不为null
			woc.setVariables(WorkDataJSON.VAR_TASKBEAN, taskBean);
		} catch (IOException e) {
			urlBean.setStatus(Status.FAIL_SAVELOCAL.name());
			urlBean.setExp(e.getClass().getCanonicalName());			
			logger.error(e.getMessage(),e);
			System.exit(1);//严重错误，直接退出
		}
	}

}
